/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.codetail.animation;

import android.os.SystemClock;
import android.support.v4.util.SimpleArrayMap;
import android.view.Choreographer;

import java.util.ArrayList;

/**
 * This custom, static handler handles the timing pulse that is shared by all active
 * ValueAnimators. This approach ensures that the setting of animation values will happen on the
 * same thread that animations start on, and that all animations will share the same times for
 * calculating their values, which makes synchronizing animations possible.
 *
 * The handler uses the Choreographer by default for doing periodic callbacks. A custom
 * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
 * may be independent of UI frame update. This could be useful in testing.
 *
 * @hide
 */
class AnimationHandler {
  /**
   * Callbacks that receives notifications for animation timing
   */
  interface AnimationFrameCallback {
    /**
     * Run animation based on the frame time.
     *
     * @param frameTime The frame start time
     */
    boolean doAnimationFrame(long frameTime);
  }

  /**
   * Internal per-thread collections used to avoid set collisions as animations start and end
   * while being processed.
   *
   * @hide
   */
  private final SimpleArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
      new SimpleArrayMap<>();
  public static final ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
  private final ArrayList<AnimationFrameCallback> mAnimationCallbacks = new ArrayList<>();
  private AnimationFrameCallbackProvider mProvider;

  private long mCurrentFrameTime = 0;
  private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
      mCurrentFrameTime = System.currentTimeMillis();
      doAnimationFrame(mCurrentFrameTime);
      if (mAnimationCallbacks.size() > 0) {
        getProvider().postFrameCallback(this);
      }
    }
  };

  private boolean mListDirty = false;

  public static AnimationHandler getInstance() {
    if (sAnimatorHandler.get() == null) {
      sAnimatorHandler.set(new AnimationHandler());
    }
    return sAnimatorHandler.get();
  }

  public static long getFrameTime() {
    if (sAnimatorHandler.get() == null) {
      return 0;
    }
    return sAnimatorHandler.get().mCurrentFrameTime;
  }

  /**
   * By default, the Choreographer is used to provide timing for frame callbacks. A custom
   * provider can be used here to provide different timing pulse.
   */
  public void setProvider(AnimationFrameCallbackProvider provider) {
    if (provider == null) {
      mProvider = new MyFrameCallbackProvider();
    } else {
      mProvider = provider;
    }
  }

  private AnimationFrameCallbackProvider getProvider() {
    if (mProvider == null) {
      mProvider = new MyFrameCallbackProvider();
    }
    return mProvider;
  }

  /**
   * Register to get a callback on the next frame after the delay.
   */
  public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
    if (mAnimationCallbacks.size() == 0) {
      getProvider().postFrameCallback(mFrameCallback);
    }
    if (!mAnimationCallbacks.contains(callback)) {
      mAnimationCallbacks.add(callback);
    }

    if (delay > 0) {
      mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
    }
  }

  /**
   * Removes the given callback from the list, so it will no longer be called for frame related
   * timing.
   */
  public void removeCallback(AnimationFrameCallback callback) {
    mDelayedCallbackStartTime.remove(callback);
    int id = mAnimationCallbacks.indexOf(callback);
    if (id >= 0) {
      mAnimationCallbacks.set(id, null);
      mListDirty = true;
    }
  }

  private void doAnimationFrame(long frameTime) {
    long currentTime = SystemClock.uptimeMillis();
    for (int i = 0; i < mAnimationCallbacks.size(); i++) {
      final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
      if (callback == null) {
        continue;
      }
      if (isCallbackDue(callback, currentTime)) {
        callback.doAnimationFrame(frameTime);
      }
    }
    cleanUpList();
  }

  /**
   * Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay
   * so that they can start getting frame callbacks.
   *
   * @return true if they have passed the initial delay or have no delay, false otherwise.
   */
  private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
    Long startTime = mDelayedCallbackStartTime.get(callback);
    if (startTime == null) {
      return true;
    }
    if (startTime < currentTime) {
      mDelayedCallbackStartTime.remove(callback);
      return true;
    }
    return false;
  }

  private void cleanUpList() {
    if (mListDirty) {
      for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
        if (mAnimationCallbacks.get(i) == null) {
          mAnimationCallbacks.remove(i);
        }
      }
      mListDirty = false;
    }
  }

  private int getCallbackSize() {
    int count = 0;
    int size = mAnimationCallbacks.size();
    for (int i = size - 1; i >= 0; i--) {
      if (mAnimationCallbacks.get(i) != null) {
        count++;
      }
    }
    return count;
  }

  /**
   * Default provider of timing pulse that uses Choreographer for frame callbacks.
   */
  private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

    final Choreographer mChoreographer = Choreographer.getInstance();

    @Override
    public void postFrameCallback(Choreographer.FrameCallback callback) {
      mChoreographer.postFrameCallback(callback);
    }
  }

  /**
   * The intention for having this interface is to increase the testability of ValueAnimator.
   * Specifically, we can have a custom implementation of the interface below and provide
   * timing pulse without using Choreographer. That way we could use any arbitrary interval for
   * our timing pulse in the tests.
   *
   * @hide
   */
  public interface AnimationFrameCallbackProvider {
    void postFrameCallback(Choreographer.FrameCallback callback);
  }
}
